Genetic Net

Diseño y Análisis de un Proceso de Modelación con Inteligencia Artificial para Sistemas de Trading

Msc en Ciencia de Datos

Análisis y Diseño de Algorítmos.

Juan Francisco Muñoz Elguezabal - franciscome@iteso.mx

Repositorio de Github: Link

Video Youtube: Link

Video Drive: Link


1. Descripción del problema


1.1 Introducción

Existen dos grandes formas de entender el proceso de inversión de un capital, una inversión pasiva, como los portafolios de inversión, y una inversión activa, como estrategias de trading. Al acto de comprar y/o vender un activo financiero se le conoce como “Comerciar” o hacer “trading” y en la actualidad, la gran mayoría de esta actividad se lleva a cabo electrónicamente. Cuando se lleva a cabo un proceso de inversión activa con el objetivo de incrementar el capital, generalmente se recurre a llevar a cabo la actividad de trading con fines especulativos y en una frecuencia diaria, o incluso, cuando es realizada mediante sistemas informáticos, a una frecuencia de milisegundos. Independientemente de la frecuencia, se puede optar por 2 naturaleza de estrategias de trading, una discrecional (generalmente efectuada por profesionales) y una sistemática (generalmente efectuada por sistemas informáticos).

Cuando se plantea el uso de Machine Learning para formular una estrategia de trading de naturaleza sistemática, o también conocida como un sistema de trading algorítmico, en la generalidad de los casos se piensa en modelado predictivo del precio de un activo financiero y se plantea la predicción, bajo la perspectiva de pronósticos de precios futuros, como un problema de regresión (pronóstico del siguiente precio) o de clasificación (pronóstico de un estado alcista o bajista del precio).

Para construir modelos predictivos se pueden utilizar variables de entrada exógenas (fenómenos/procesos externos al activo financiero) y variables de entrada endógenas (transformaciones matemáticas a la misma serie de tiempo del precio). Una vez diseñadas estas variables de entrada a través de un proceso conocido como Ingeniería de Variables, se conducen actividades como Importancia y Selección de variables, al final de los cuales se logra tener variables explicativas candidatas a ser utilizadas como entrada en los modelos propuestos. Es sabido que para modelar procesos financieros como el precio de activos bursátiles se necesita contar tanto con variables lineales como no lineales, así como con modelos de naturaleza lineal y no lineal. En el proceso de uso de variables, ajuste de modelos, optimización y backtest de los hiperparámetros es

Finalmente, las técnicas de validación cruzada son frecuentemente utilizadas en problemas de optimización de modelos, Sin embargo se debe de tener una consideración especial cuando se trata de datos del tipo series de tiempo, esto es debido a que el orden de los datos debe de ser preservado durante el proceso de generación de subconjuntos de entrenamiento y prueba. Para abordar esta situación, se propone utilizar un método conocido como “Blocking Time Series Split” o División por Bloques de Series de Tiempo.

1.2 Problemática

  • Variables explicativas
  • División de conjuntos
  • Optimización de hiperparámetros
  • Visualización de resultados
  • Error de Generalización en Modelos Predictivos

1.3 Hipótesis

  1. Variables endógenas son suficientes para pronosticar bien
  2. Optimización en T-Folds
  3. Estabilidad paramétrica
  4. Resultados +70%

1.4 Experimento

Precios de futuros continuos, variables endógenas de 3 tipos, divisiónm de t-folds, optimización con algoritmos genéticos, estabilidad paramétrica, desempeño final.

2. Solución propuesta


2.1 Librerías

Para la elaboración de este proyecto se utilizaron las siguientes liberías, las cuales, es necesario tenerlas instaladas y/o en el archivo requirements.txt con el siguiente contenido :

  • pandas>=1.1.0
  • numpy>=1.19.1
  • jupyter>=1.0.0
  • deap>=1.3.1
  • gplearn>=0.4.1
  • requests>=2.24.0
  • plotly>=4.12.0
  • scikit-learn>=0.23.2
  • chart_studio>=1.1.0

2.2 Dependencias

La distribución de la funcionalidad de todo el proyecto fue un aspecto importante a considerar para mantener modularidad, eficiencia e interpretabilidad en este notebook. La organización del proyecto y las dependencias es la siguiente:

In [1]:
%%capture
"""
- files
- - daily_prices : Precios historicos por día
- - minute_prices : Precios historicos por minuto
- - images : Imagenes Utilizadas en notebook
- - pickle_rick : Resultados para analisis de complejidad

- main.py : Secuencia principal del proyecto
- data.py : Entrada y salida de datos
- functions.py : operaciones, modelos, preprocesamiento de datos
- visualizations.py : graficas

- Genetic_Net_Report.ipynb : Este notebook
- Genetic_Net_Report.html : Version "Stand Alone" del notebook

- venv : Ambiente Virtual del Proyecto
- requirements.txt : Requerimientos de librerias
- README.md : Para repositorio
- .gitignore : Para repositorio
"""
In [2]:
%%capture
pip install -r requirements.txt

2.3 Cargar librerías y dependencias

Cargar librerias y dependencias a utilizar en este notebook

In [89]:
# -- Librerias necesarias para este notebook
import warnings
from datetime import datetime
import pandas as pd
warnings.filterwarnings("ignore")

# -- Dependencias (scripts) necesarias para este notebook
import visualizations as vs
import data as dt
import functions as fn

NOTA

Debido a que la naturaleza de este proyecto implicó hacer una búsqueda quasi-exhaustiva de condiciones y utilizando una gran cantidad de datos, el tiempo que demora el proyecto completo en generar los resultados fue de mas de 6 horas por cada variación de condiciones (se hicieron 3 variaciones que mas adelante se explican), por lo tanto, se agregan las siguientes notas:

  1. El contenido de este notebook

Lo que se muestra en este notebook, tanto de contenido, como de códigos, es sólo una parte de todo el proyecto. Debido a la naturaleza compleja del planteamiento de la solución y la interconectividad entre funcionalidades construidas, se generaron +2,000 lineas de código. También, las gráficas y tablas que se muestran, en su mayoría, son sólo la visualización de un caso particular de variación. En este trabajo se hicieron 3 variaciones del algoritmo/proceso de modelación general: Periodos Trimestrales, Semestrales y Anuales.

  1. Los archivos con resultados almacenados

Todos los datos generados en el proceso completo para cada variación se guardaron en archivos tipo "pickle", un formato para almacenamiento de ambientes para python, estos se encuentran en la carpeta pickle_rick.

In [27]:
# All the saved data
memory_palace = dt.data_save_load(p_data_objects=None, p_data_action='load',
                                  p_data_file='files/pickle_rick/Genetic_Net_Quarter.dat')

2.4 Descripcion de datos

El dataset principal del proyecto fueron los precios diarios de los contratos futuros M6XXX del tipo de cambio UsdMxn, se hizo una unión calendario de varios contratos para obtener una serie de tiempo de contratos futuros continuos. La representacion de estos precios es la conocida como de velas japonesas o Candlesticks con los precios de OHLC (Open, High, Low, Close). Se tiene precios que abarcan desde 2010-01-03 hasta 2020-10-30

In [4]:
# General dataframe with all the data for the project
general_data = dt.ohlc_data.copy()

# visualizar datos
general_data
Out[4]:
timestamp open high low close volume
0 2010-01-03 19.65988 19.65988 19.61169 19.61169 84
1 2010-01-04 19.60208 19.60208 19.15158 19.17913 19823
2 2010-01-05 19.16076 19.21599 18.96993 19.05125 14243
3 2010-01-06 19.04218 19.08761 18.80937 18.87149 12881
4 2010-01-07 18.85370 19.00599 18.81822 18.88931 12960
... ... ... ... ... ... ...
3362 2020-10-26 21.07038 21.24044 21.03049 21.04377 51816
3363 2020-10-27 21.03934 21.20441 20.95118 21.16402 42135
3364 2020-10-28 21.16850 21.46383 21.12825 21.30379 85099
3365 2020-10-29 21.30379 21.57497 21.24044 21.43163 63725
3366 2020-10-30 21.43163 21.58895 21.28112 21.28565 52380

3367 rows × 6 columns

In [5]:
# ------------------------------------------------------------- PLOT 1: MXN/USD Historical Future Prices -- #
# --------------------------------------------------------------- -------------------------------------- -- #

# Override plot main title
dt.theme_plot_1['p_labels']['title'] = 'Gráfica 1: <b> Precios Históricos OHLC </b>'

# Plot 1 : time series candlesticks OHLC historical prices
plot_1 = vs.g_ohlc(p_ohlc=general_data, p_theme=dt.theme_plot_1, p_vlines=None)

# Render plot
plot_1
In [14]:
# data description
table_1 = general_data.describe()

# visualize response in console
table_1
Out[14]:
open high low close volume
count 3367.000000 3367.000000 3367.000000 3367.000000 3367.000000
mean 19.729345 19.850853 19.616502 19.730419 32022.940006
std 2.877919 2.915340 2.842204 2.878982 21894.216553
min 15.062510 15.165300 15.006000 15.056840 9.000000
25% 16.959210 17.056845 16.880490 16.962810 17568.000000
50% 19.516000 19.619380 19.402410 19.519810 31230.000000
75% 21.853145 21.973190 21.706100 21.855535 44653.500000
max 28.579590 28.776980 28.441410 28.579590 176260.000000

2.5 Division de datos

Para poder conducir el proceso de optimización y reducir el error de generalización de los modelos predictivos, se utilizó una técnica conocida como validación cruzada, con la consideración especial que los datos son series de tiempo, y que por lo tanto, debido a que el orden si importa, la división de los datos para la validación cruzada es secuencial y sin filtraciones. Esto último significa que, en cada periodo, se conduce el proceso de separación de datos de entrenamiento y prueba, se hace la ingeniería de variables, se hace la optimización con datos de entrenamiento se obtienen métricas tanto para los datos de entrenamiento como los de prueba y se grafican, todo esto sin que los datos de un periodo se "filtren" en los de otro periodo.

Los 3 tamaños de bloques utilizados para la validación cruzada fueron:

  1. Periodos Trimestrales
  2. Periodos Semestrales
  3. Periodos Anuales
In [15]:
%%capture
# in quarters obtain 4 folds for each year
t_folds = fn.t_folds(p_data=general_data.copy(), p_period='quarter')

# drop the last quarter because it is incomplete until december 31
t_folds.pop('q_04_2020', None)
In [26]:
print('Un ejemplo de las primeras 5 llaves del diccionario con todos los T-Folds:', list(t_folds.keys())[0:4])
Un ejemplo de las primeras 5 llaves del diccionario con todos los T-Folds: ['q_01_2010', 'q_02_2010', 'q_03_2010', 'q_04_2010']
In [16]:
# construccion de fechas para lineas verticales de division de cada fold
dates_folds = []
for fold in list(t_folds.keys()):
    dates_folds.append(t_folds[fold]['timestamp'].iloc[0])
    dates_folds.append(t_folds[fold]['timestamp'].iloc[-1])

# Override plot main title
dt.theme_plot_1['p_labels']['title'] = 'Gráfica 2: <b> ' + \
'T-Folds por Bloques Sin Filtraciones para Series de Tiempo </b>'

# grafica OHLC
plot_2 = vs.g_ohlc(p_ohlc=general_data, p_theme=dt.theme_plot_1, p_vlines=dates_folds)

# visualize
plot_2

2.6 Ingeniería de Variables

Se generaron tres tipos de variables explicativas:

  1. Autoregresivas

Con la función autoregressive_features, son promedios móviles y operadores resago de los precios Open, High, Low, Close y del Volumen

  1. Hadamard

El código de esta operación está en la función hadamard_features y consiste en la multiplicación elemento por elemento (producto Hadamard de dos matrices) de las columnas previamente generadas.

  1. Symbólicas

En la función symbolic_features se puede observar el uso de una función especial, SymbolicTransformer, de la librería "GPLearn". Esta función especial es el encapsulamiento de un proceso de programación genética para la generación de "programas" o "ecuaciones simbólicas" que generan nuevos features.

Las operaciones simbólicas utilizadas para la generación de estos features fueron:

  • Substraction (sub) : $A - B$
  • Addition (add) : $A + B$
  • Multiplication (mul) : $A \cdot B$
  • Inverse (inv) : $A^{-1}$
  • Absolute value (abs) : $|A|$
  • Logarithm (log) : $log(A)$
In [30]:
memory_palace['ls-svm']['q_01_2010']['features']['train_x'].head(5)
Out[30]:
lag_vol_1 lag_ol_1 lag_ho_1 lag_hl_1 ma_vol_1 ma_ol_1 ma_ho_1 ma_hl_1 lag_vol_2 lag_ol_2 ... sym_20 sym_21 sym_22 sym_23 sym_24 sym_25 sym_26 sym_27 sym_28 sym_29
0 -1.452195 378.691874 -371.883259 6.808615 -0.966560 -194.994169 901.274493 706.280325 -0.784202 168.776160 ... -251.344895 486.531913 -230.500422 111.407925 443.231842 -844.164772 -164.031907 -1046.774480 -355.056542 502.380562
1 -0.966560 -194.994169 901.274493 706.280325 -0.716229 14.334693 268.632741 282.967434 -1.452195 378.691874 ... -971.677071 282.581966 -458.721835 397.184344 -768.237745 -257.066155 -27.961000 0.720052 768.717091 975.785184
2 -0.716229 14.334693 268.632741 282.967434 -0.898404 512.162506 -322.220976 189.941530 -0.966560 -194.994169 ... -80.999233 -70.326070 38.910271 28.116413 414.914933 374.502287 464.469541 801.588033 -269.577974 130.835489
3 -0.898404 512.162506 -322.220976 189.941530 -0.762504 45.263227 82.522834 127.786061 -0.716229 14.334693 ... -10.701067 286.079154 115.841366 -189.977321 -176.556119 -104.145666 -45.165371 119.397129 181.154662 12.766038
4 -0.762504 45.263227 82.522834 127.786061 -0.995979 106.634205 -284.366929 -177.732723 -0.898404 512.162506 ... 45.008997 24.431608 72.283279 -242.921020 195.966248 321.090355 69.951124 250.253598 -170.641010 56.598113

5 rows × 310 columns

2.7 Modelos predictivos

Se utilizaron 3 tipos de modelos predictivos:

  1. Regresión logística con regularización tipo Elastic Net (logistic_net)
  2. Máquinas de Soporte Vectorial con regularización L1 (l1_svm)
  3. Red Neuronal Artificial Perceptrón Multicapa (ann-mlp)

Se utilizaron las funcionalidades de la librería scikitlearn : LogisticRegression, SVC, MLPClassifier

2.8 Proceso de optimización y validación cruzada

Los hiperparametros que se optimizaron para cada modelo, y los valores que se iteraron para cada uno de ellos, fueron los siguientes:

In [18]:
models = {
    'ann-mlp': {
        'label': 'ann-mlp',
        'params': {'hidden_layers': [(5, ), (10, ), (5, 5), (10, 5), (10, 10),
                                     (5, ), (10, ), (5, 5), (10, 5), (10, 10)],
                   'activation': ['relu', 'relu', 'relu', 'relu', 'relu',
                                  'logistic', 'logistic', 'logistic', 'logistic', 'logistic'],
                   'alpha': [0.005, 0.1, 0.05, 0.02, 0.01, 0.005, 0.1, 0.05, 0.02, 0.01],
                   'learning_r': ['constant', 'constant', 'constant', 'constant', 'constant',
                                  'adaptive', 'adaptive', 'adaptive', 'adaptive', 'adaptive'],
                   'learning_r_init': [0.2, 0.1, 0.02, 0.01, 0.001, 0.2, 0.1, 0.02, 0.01, 0.001]}},

    'logistic-elasticnet': {
        'label': 'logistic-elasticnet',
        'params': {'ratio': [0.05, 0.10, 0.20, 0.30, 0.50, 0.60, 0.70, 0.80, 0.90, 1.00],
                   'c': [1.5, 1.1, 1, 0.8, 0.5, 1.5, 1.1, 1, 0.8, 0.5]}},

    'ls-svm': {
        'label': 'ls-svm',
        'params': {'c': [1.5, 1.1, 1, 0.8, 0.5, 1.5, 1.1, 1, 0.8, 0.5],
                   'kernel': ['linear', 'linear', 'linear', 'linear', 'linear',
                              'rbf', 'rbf', 'rbf', 'rbf', 'rbf'],
                   'gamma': ['scale', 'scale', 'scale', 'scale', 'scale',
                             'auto', 'auto', 'auto', 'auto', 'auto']}}}

El proceso de optimización se realizó con algorítmos genéticos, utilizando los constructores de la librería DEAP, la cual, permite especificar funciones hechas a la medida así como utilizar algunas predefinidas que se ofrecen. A continuación se incluye una parte de la función genetic_algo_optimization

In [ ]:
# if p_model['label'] == 'logistic-elasticnet':
# 
#     # borrar clases previas si existen
#     try:
#         del creator.FitnessMax_en
#         del creator.Individual_en
#     except AttributeError:
#         pass
# 
#     # inicializar ga
#     creator.create("FitnessMax_en", base.Fitness, weights=(1.0,))
#     creator.create("Individual_en", list, fitness=creator.FitnessMax_en)
#     toolbox_en = base.Toolbox()
# 
#     # define how each gene will be generated (e.g. criterion is a random choice from the criterion list).
#     toolbox_en.register("attr_ratio", random.choice, p_model['params']['ratio'])
#     toolbox_en.register("attr_c", random.choice, p_model['params']['c'])
# 
#     # This is the order in which genes will be combined to create a chromosome
#     toolbox_en.register("Individual_en", tools.initCycle, creator.Individual_en,
#                         (toolbox_en.attr_ratio, toolbox_en.attr_c), n=1)
# 
#     # population definition
#     toolbox_en.register("population", tools.initRepeat, list, toolbox_en.Individual_en)
# 
#     # -------------------------------------------------------------- funcion de mutacion para LS SVM -- #
#     def mutate_en(individual):
# 
#         # select which parameter to mutate
#         gene = random.randint(0, len(p_model['params']) - 1)
# 
#         if gene == 0:
#             individual[0] = random.choice(p_model['params']['ratio'])
#         elif gene == 1:
#             individual[1] = random.choice(p_model['params']['c'])
# 
#         return individual,
# 
#     # --------------------------------------------------- funcion de evaluacion para OLS Elastic Net -- #
#     def evaluate_en(eva_individual):
# 
#         # output of genetic algorithm
#         chromosome = {'ratio': eva_individual[0], 'c': eva_individual[1]}
# 
#         # model results
#         model = logistic_net(p_data=p_data, p_params=chromosome)
# 
#         # True positives in train data
#         train_tp = model['results']['matrix']['train'][0, 0]
#         # True negatives in train data
#         train_tn = model['results']['matrix']['train'][1, 1]
#         # Model accuracy
#         train_fit = (train_tp + train_tn) / len(model['results']['data']['train'])
# 
#         # True positives in test data
#         test_tp = model['results']['matrix']['test'][0, 0]
#         # True negatives in test data
#         test_tn = model['results']['matrix']['test'][1, 1]
#         # Model accuracy
#         test_fit = (test_tp + test_tn) / len(model['results']['data']['test'])
# 
#         # Fitness measure
#         model_fit = np.mean([train_fit, test_fit])
# 
#         return model_fit,
# 
#     toolbox_en.register("mate", tools.cxOnePoint)
#     toolbox_en.register("mutate", mutate_en)
#     toolbox_en.register("select", tools.selTournament, tournsize=10)
#     toolbox_en.register("evaluate", evaluate_en)
# 
#     population_size = 60
#     crossover_probability = 0.8
#     mutation_probability = 0.1
#     number_of_generations = 1
# 
#     en_pop = toolbox_en.population(n=population_size)
#     en_hof = tools.HallOfFame(10)
#     stats = tools.Statistics(lambda ind: ind.fitness.values)
#     stats.register("avg", np.mean)
#     stats.register("std", np.std)
#     stats.register("min", np.min)
#     stats.register("max", np.max)
# 
#     # Genetic Algorithm Implementation
#     en_pop, en_log = algorithms.eaSimple(population=en_pop, toolbox=toolbox_en, stats=stats,
#                                          cxpb=crossover_probability, mutpb=mutation_probability,
#                                          ngen=number_of_generations,
#                                          halloffame=en_hof, verbose=True)
# 
#     return {'pop': en_pop, 'logs': en_log, 'hof': en_hof}

3. Pruebas y resultados


In [38]:
# Predictive models
ml_models = list(dt.models.keys())
ml_models
Out[38]:
['ann-mlp', 'logistic-elasticnet', 'ls-svm']

El siguiente es un ejemplo de como se iba imprimiendo en la consola el resultado de las principales operaciones con algoritmos. Podemos observar que para este ejemplo se estaba explorando un tipo de periodicidad semestral, de ahi el inidicativo s_ en el nombre del periodo, El modelo probado estaba siendo la red neuronal tipo perceptrón multicapa.

  1. Programación genética para ingeniería de variables.

Sólo se tuvieron que crear 2 generaciones para llegar a encontrar un individuo cuyo cromosoma lograra producir un fitness igual o mayor al especificado, que en este caso era el programa creado generara una variable explicativa que tuviera, por lo menos, un 0.60 de coeficiente de correlación de perason con la variable objetivo (como era binaria, el coeficiente de correlación fue una buena opción de fitness).

  1. Algorítmos genéticos para optimización de hiperparámetros.

En el proceso de optimización, también sólo fueron necesarias 2 generaciones, y de todos los individuos de cada generación, se obtuvieron medidas estadísticas de dispersión y de tendencia central, con la finalidad de observar el mejoramiento "entre generaciones", mas allá de sólo buscar el "mejor" individuo, empíricamente se buscó que se estuvieran teniendo cada ves mejores generaciones y así tener una heurística de estabilidad paraḿetrica.

Finalmente se registró el tiempo que transcurrió este proceso, fue un poco mas de 14 minutos con 42 segundos.

In [ ]:
# ----------------------------
# modelo:  ann-mlp
# periodo:  s_01_2010
# ----------------------------
# 
# ----------------------- Ingenieria de Variables por Periodo ------------------------
# ----------------------- ----------------------------------- ------------------------
#     |   Population Average    |             Best Individual              |
# ---- ------------------------- ------------------------------------------ ----------
# Gen   Length          Fitness   Length          Fitness      OOB Fitness  Time Left
#    0    64.14        0.0784983        3         0.542514              N/A      5.82m
#    1     3.64         0.160435        5         0.605034              N/A      8.77m
# 
# --------------------- Optimizacion de hiperparametros por Periodo ------------------
# --------------------- ------------------------------------------- ------------------
# gen	nevals	avg     	std      	min     	max
# 0  	60    	0.580082	0.0396069	0.518627	0.755882
# 1  	50    	0.652168	0.0653966	0.497712	0.766993
# 
# Elapsed Time = 0:14:42.882302

3.1 Definción de mejores casos

Para los 10 años de precios diarios que se exploraron, una vez que estos fueron divididos en T-Folds (trimestral, semestral, anual), para cada periodo se conducian los siguientes subprocesos:

  1. Ingeniería de variables
  2. Ajuste de modelo
  3. Optimización de hiperparámetros
  4. Backtest de resultados
  5. Cálculo de métricas de desempeño

Y se almacenaba el "Hall of Fame" de cada periodo, el cual, consiste en un grupo con los 10 mejores individuos de la última generación obtenida en el periodo de optimización, con estos 10 individuos se evaluaba el modelo con tales parámetros y se almacenaban esos resultados. De tal manera que para encontrar los "mejores de los mejores" se buscaron, en todos los periodos, para los 3 modelos, aquellos individuos o configuraciones de parámetros de los modelos que dieran el mejor AUC y el peor AUC. Mientras mas cercana este la AUC de un modelo al valor de 1, es mejor.

3.2 Casos encontrados

Después de haber conducido el proceso de optimización para todos y cada uno de los periodos, y para todos y cada uno de los modelos, con tales resultados, se buscaron los casos donde se tuviera un AUC maximo y uno minimo

In [33]:
auc_cases = fn.models_auc(p_models=ml_models, p_global_cases=memory_palace, p_data_folds=t_folds)

3.3 Ejemplo de caso

In [53]:
# pick case
case = 'max'

# pick model to generate the plot
auc_model = 'ann-mlp'
In [57]:
# get train and test y data 
train_y = auc_cases[auc_model]['auc' + '_' + case]['data']['results']['data']['train']
test_y = auc_cases[auc_model]['auc' + '_' + case]['data']['results']['data']['test']

# get data for prices and predictions
ohlc_prices = t_folds[auc_cases[auc_model]['auc' + '_' + case]['period']]
ohlc_class = {'train_y': train_y['y_train'], 'train_y_pred': train_y['y_train_pred'],
              'test_y': test_y['y_test'], 'test_y_pred': test_y['y_test_pred']}

# print dataframe
ohlc_prices
Out[57]:
timestamp open high low close volume
0 2015-07-01 19.83340 19.93223 19.77457 19.88072 42242
1 2015-07-02 19.88072 19.96805 19.66182 19.73165 55539
2 2015-07-03 19.73165 19.78631 19.69667 19.75504 4010
3 2015-07-05 19.84915 19.91239 19.87281 19.90050 1980
4 2015-07-06 19.90050 20.00800 19.75114 19.77457 49452
... ... ... ... ... ... ...
74 2015-09-25 21.52853 21.70610 21.22692 21.64034 45164
75 2015-09-27 21.68727 21.66847 21.60294 21.62630 544
76 2015-09-28 21.62162 21.90101 21.55172 21.87227 33588
77 2015-09-29 21.87227 21.91541 21.62162 21.64971 54107
78 2015-09-30 21.64971 21.66378 21.43163 21.44082 34017

79 rows × 6 columns

In [76]:
# train vline
train_vline = ohlc_prices['timestamp'].iloc[int(round(len(ohlc_prices)*.70, 0))]

# generate title
auc_title = 'max AUC for: ' + auc_model + ' found in period: ' + \
             auc_cases[auc_model]['auc_' + case]['period']

# Override plot main title
dt.theme_plot_2['p_labels']['title'] = auc_title

# make plot
plot_en = vs.g_ohlc_class(p_ohlc=ohlc_prices, p_theme=dt.theme_plot_2,
                          p_data_class=ohlc_class, p_vlines=[train_vline])

# visualize plot
plot_en

En el caso de este ejemplo fue de 1 trimestre de precios diarios, el modelo de la Red neuronal, y el periodo en el que se encontro el "Máximo de máximos" según la cifra de AUC. Podemos observar que las velas rojas fueron los errores del modelo y las azules los aciertos. Del lado izquierdo de la línea vertical se encuentra el periodo de entrenamiento para ese T-Fold y del lado derecho de la línea vertical el periodo de prueba.

In [63]:
# Model accuracy (in sample)
model_acc_train = round(auc_cases[auc_model]['auc' + '_' + case]['data']['metrics']['train']['auc'], 2)
print('The model AUC with train data was: ', model_acc_train)

# Model accuracy (out of sample)
model_acc_test = round(auc_cases[auc_model]['auc' + '_' + case]['data']['metrics']['test']['auc'], 2)
print('\nThe model AUC with test data was: ', model_acc_test)
The model AUC with train data was:  0.76

The model AUC with test data was:  0.91
In [64]:
# Model accuracy (in sample)
model_acc_train = round(auc_cases[auc_model]['auc' + '_' + case]['data']['metrics']['train']['acc']*100, 2)
print('The model accuracy with train data was: ', model_acc_train, '%')

# Model accuracy (out of sample)
model_acc_test = round(auc_cases[auc_model]['auc' + '_' + case]['data']['metrics']['test']['acc']*100, 2)
print('\nThe model accuracy with test data was: ', model_acc_test, '%')
The model accuracy with train data was:  72.0 %

The model accuracy with test data was:  90.91 %

2.8 Curva ROC y AUC

Se presentan las curvas ROC de los "Mejores de los mejores" para cada modelo

In [78]:
# generate title
auc_title = 'ROC de Mejores y Peores Casos (datos de prueba)'

# Override plot main title
dt.theme_plot_4['p_labels']['title'] = auc_title

# generacion de grafica
plot_4_folds = vs.g_roc_auc(p_cases=auc_cases, p_type='test', p_models=ml_models, p_theme=dt.theme_plot_4)

# imprimir grafica
plot_4_folds 

2.9 Estabilidad de AUC

In [65]:
minmax_auc_test = {i: {'x_period': [], 'y_mins': [], 'y_maxs': []} for i in ml_models}

# get the cases where auc was min and max in all the periods
for model in ml_models:
    minmax_auc_test[model]['x_period'] = list(auc_cases[model]['hof_metrics']['data'].keys())
    minmax_auc_test[model]['y_mins'] = [auc_cases[model]['hof_metrics']['data'][periodo]['auc_min']
                                        for periodo in list(auc_cases[model]['hof_metrics']['data'].keys())]
    minmax_auc_test[model]['y_maxs'] = [auc_cases[model]['hof_metrics']['data'][periodo]['auc_max']
                                        for periodo in list(auc_cases[model]['hof_metrics']['data'].keys())]

# produce plot
plot_5 = vs.g_timeseries_auc(p_data_auc=minmax_auc_test, p_theme=dt.theme_plot_5)

plot_5

2.10 Estabilidad paramétrica

In [66]:
# stable data
data_stables = {model: {'df_auc_max': {period: {} for period in t_folds},
                        'df_auc_min': {period: {} for period in t_folds}} for model in ml_models}

# periods
period_max_auc = {model: {period: {} for period in t_folds} for model in ml_models}
period_min_auc = {model: {period: {} for period in t_folds} for model in ml_models}

# cycle for getting the parameters of every model
for model in ml_models:
    for period in list(t_folds.keys()):
        period_max_auc[model][period] = auc_cases[model]['hof_metrics']['data'][period]['auc_max_params']
        period_min_auc[model][period] = auc_cases[model]['hof_metrics']['data'][period]['auc_min_params']

# Table 2: Model parameters
table_2 = {'model_1': {'max': pd.DataFrame(period_max_auc['logistic-elasticnet']).T,
                       'min': pd.DataFrame(period_min_auc['logistic-elasticnet']).T},
           'model_2': {'max': pd.DataFrame(period_max_auc['ls-svm']).T,
                       'min': pd.DataFrame(period_min_auc['ls-svm']).T},
           'model_3': {'max': pd.DataFrame(period_max_auc['ann-mlp']).T,
                       'min': pd.DataFrame(period_min_auc['ann-mlp']).T}}

# evolution of parameters for every  model
t_model_1 = table_2['model_1']['max']
t_model_2 = table_2['model_2']['max']
t_model_3 = table_2['model_3']['max']
t_model_3['hidden_layers'] = [str(i) for i in list(t_model_3['hidden_layers'])]

2.10.1 Evolución paramétrica logistic-net

Evolución de parámetros para modelo: Regresión Logística con Regularización Elastic Net (logistic-net)

In [70]:
t_model_1.T
Out[70]:
q_01_2010 q_02_2010 q_03_2010 q_04_2010 q_01_2011 q_02_2011 q_03_2011 q_04_2011 q_01_2012 q_02_2012 ... q_02_2018 q_03_2018 q_04_2018 q_01_2019 q_02_2019 q_03_2019 q_04_2019 q_01_2020 q_02_2020 q_03_2020
ratio 0.7 0.7 0.7 0.7 0.7 0.05 0.05 0.05 0.05 0.05 ... 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05
c 1.5 1.5 1.5 1.5 1.5 1.10 1.10 1.10 1.10 1.10 ... 1.10 1.10 1.10 1.10 1.10 1.10 1.10 1.10 1.10 1.10

2 rows × 43 columns

2.10.2 Evolución paramétrica l1-svm

Evolución de parámetros para modelo: Máquina de Soporte Vectorial con Regularización L1

In [75]:
t_model_2.T
Out[75]:
q_01_2010 q_02_2010 q_03_2010 q_04_2010 q_01_2011 q_02_2011 q_03_2011 q_04_2011 q_01_2012 q_02_2012 ... q_02_2018 q_03_2018 q_04_2018 q_01_2019 q_02_2019 q_03_2019 q_04_2019 q_01_2020 q_02_2020 q_03_2020
c 1.1 1.1 0.8 0.8 0.8 1.5 1.5 1.1 1.1 1.1 ... 0.8 0.8 0.8 0.8 0.8 0.8 0.8 0.8 0.8 0.8
kernel rbf rbf linear linear linear linear linear linear linear linear ... linear linear linear linear linear linear linear linear linear linear
gamma auto auto auto auto auto auto auto auto auto auto ... scale scale scale scale scale scale scale scale scale scale

3 rows × 43 columns

2.10.3 Evolución paramétrica ann-mlp

Evolución de parámetros para modelo: Red Neuronal Artificial tipo Perceptrón Multicapa

In [73]:
t_model_3.T
Out[73]:
q_01_2010 q_02_2010 q_03_2010 q_04_2010 q_01_2011 q_02_2011 q_03_2011 q_04_2011 q_01_2012 q_02_2012 ... q_02_2018 q_03_2018 q_04_2018 q_01_2019 q_02_2019 q_03_2019 q_04_2019 q_01_2020 q_02_2020 q_03_2020
hidden_layers (10, 5) (10, 5) (5,) (5,) (5,) (5,) (5,) (5,) (5,) (5,) ... (10, 10) (10, 10) (10, 10) (10, 10) (10, 10) (10, 10) (10, 10) (10, 10) (10, 10) (10, 10)
activation logistic logistic logistic logistic logistic logistic logistic logistic logistic logistic ... logistic logistic logistic logistic logistic logistic logistic logistic logistic logistic
alpha 0.05 0.05 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 ... 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05
learning_r constant constant constant constant constant constant constant constant constant constant ... adaptive adaptive adaptive adaptive adaptive adaptive adaptive adaptive adaptive adaptive
learning_r_init 0.2 0.2 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 ... 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2

5 rows × 43 columns

4. Análisis de complejidad


Se hizo un analisis de la complejidad espacial de correr el algoritmo para los 3 modelos, encontras los mejores casos, encontrar el mejor de esos mejores casos para cada modelo y encontrar el mejor entre los modelos (todo el proceso anteriormente explicado).

In [123]:
import numpy as np
import matplotlib.pyplot as plt

# object for time keeping
times = {'quarter': {'whole_period': [], 'mean_periods': [], 'periods': []},
         'semester': {'whole_period': [], 'mean_periods': [], 'periods': []},
         'year': {'whole_period': [], 'mean_periods': [], 'periods': []}}
       
for size in times.keys():
    # in quarters obtain 4 folds for each year
    t_folds = fn.t_folds(p_data=general_data.copy(), p_period=size)
    
    if size == 'quarter':
        t_folds.pop('q_04_2020', None)
    
    # load data
    memory_palace = dt.data_save_load(p_data_objects=None, p_data_action='load',
                                      p_data_file='files/pickle_rick/Genetic_Net_' + size + '.dat')
       
    # list with the names of the models
    ml_models = list(dt.models.keys())
    period_times = list()
    # iterate to have all the times for each period for each model
    times_model = {}
    for model in ml_models:
        times_model[model] = times
        for period in list(t_folds.keys()):
            period_times.append(memory_palace[model][period]['time'].seconds/60)
        times_model[model][size]['whole_period'].append(np.sum(period_times))
        times_model[model][size]['periods'].append(period_times)
        times_model[model][size]['mean_periods'].append(np.mean(period_times))


ind = [1, 2, 3]
# ajuste polinomial a promedios de valores de count
pl = np.polyfit(x=np.arange(0, len(ind)), y=times['quarter']['whole_period'], deg=1)
pl = np.poly1d(pl)

# construccion de grafica
plt.plot(ind, times['quarter']['whole_period'], 'b*')

plt.plot(ind, pl(np.arange(0, len(ind))), 'r-')
plt.suptitle("Analisis de complejidad $(posteriori)$")
plt.title('logistic-net + l1-svm + ann-mlp', fontsize=10)
plt.legend(["Tiempos", "$O(N)$"])
plt.xlabel("Tamaño DataSet (1=Trim, 2=Sem, 3=Anual)")
plt.ylabel("Minutos en correr 10 años")

# construccion de ecuacion para O(n)
eqn = str(round(list(pl.coefficients)[0], 1)) + 'x' + ' + ' + \
      str(round(list(pl.coefficients)[1], 1)) 

plt.text(1.5, 200, '$y=' + eqn + '$', {'color': 'r', 'fontsize': 8})

# mostrar plot
plt.show()

El resultado que se puede observar es que el tiempo aumentará linealmente conforme se aumente linealmente el tamaño de los data sets, y esto se puede observar con los datos mostrados aún y que los periodos fueron solamente 3 en esta entrega (trimestrales, semestrales, anuales). El tiempo aumenta conforme al tamaño de los data set para hacer todo el proceso diseñado, desde crear los T-Folds, pasando por generación de variables y optimización de hiperparámetros, hasta el cálculo de métricas y visualizaciones.

5. Conclusiones


Es importante notar que el principal resultado de este trabajo es el beneficio de tener un híbrido entre framework y algoritmo. El primero debido a que el marco teórico sobre el uso de variables explicativas lineales y separación de conjuntos de entrenamiento y prueba con T-Folds para series de tiempo es una base conceptual útil para construir las siguientes componentes. Un algoritmo debido a que se considera programación genética, algorítmos genéticos y Machine Learning para la construcción de modelos predictivos, y todo esto, se hace bajo un esquema de ciclos, los cuales, están preponderantemente determinados por el tamaño y cantidad de los conjuntos de entrenamiento, así como de algunas características del proceso de programación genética, como el criterio del fitness, y otros aspectos de los algorítmos genéticos, como la cantidad de generaciones, el Hall of Fame y el criterio de AUC para encontrar el "Mejor entre los mejores".

De acuerdo al presente planteamiento, se puede concluir también que el uso de inteligencia artificial, por si solo, no fue siempre productivo, como se pudo apreciar en los peores casos donde se obtuvo valores de AUC muy bajos. El entender como realizar el proceso de generación de variables fue tan importante como el de optimización de hiperparámetros, y ambos, fueron habilitados gracias al criterio de división de datos.

Como trabajo a futuro se queda el generar mas sub-divisiones de datos para extender el análisis de complejidad.